﻿using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RaytracerLib.MathLib;
using TomanuExtensions;
using RaytracerLib;
using System;
using RaytracerLib.MathLib.Polynomials;
using RaytracerLib.MathLib.PolynomialFunction;

namespace RaytracerTest
{
    [TestClass]
    public class PolynomialFunctionTest : TestBase
    {
        private Vector3[] m_random_vectors;
        private double[] m_random_dists;

        [TestInitialize]
        public void Initialize()
        {
            m_random_dists = new[] { 4.345, 1, 1.2546e-6, 0 };

            m_random_dists = m_random_dists.Concat(m_random_dists.Select(v => -v)).Distinct().ToArray();

            m_random_vectors = (from x in m_random_dists
                                from y in m_random_dists
                                from z in m_random_dists
                                select new Vector3(x, y, z)).ToArray();
        }

        [TestMethod]
        public void PolynomialFunction_Test()
        {
            NumberParsingTest();
            NumberEvaluationTest();
            SimpleExamplesTest();
            ExpandExamplesTest();
        }

        private void NumberParsingTest()
        {
            {
                var pf = new PolynomialFunction("x++12.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x+123404");
            }

            {
                var pf = new PolynomialFunction("x++12.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.001234");
            }

            {
                var pf = new PolynomialFunction("x++12.34e4+4");
                Assert.AreEqual(pf.ToString(), "x+123404");
            }

            {
                var pf = new PolynomialFunction("x++.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x+3404");
            }

            {
                var pf = new PolynomialFunction("x++.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.000034");
            }

            {
                var pf = new PolynomialFunction("x++.34e4+4");
                Assert.AreEqual(pf.ToString(), "x+3404");
            }

            {
                var pf = new PolynomialFunction("x++12e+4+4");
                Assert.AreEqual(pf.ToString(), "x+120004");
            }

            {
                var pf = new PolynomialFunction("x++12e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.0012");
            }

            {
                var pf = new PolynomialFunction("x++12e4+4");
                Assert.AreEqual(pf.ToString(), "x+120004");
            }

            {
                var pf = new PolynomialFunction("x+-12.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x-123396");
            }

            {
                var pf = new PolynomialFunction("x+-12.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+3.998766");
            }

            {
                var pf = new PolynomialFunction("x+-12.34e4+4");
                Assert.AreEqual(pf.ToString(), "x-123396");
            }

            {
                var pf = new PolynomialFunction("x+-.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x-3396");
            }

            {
                var pf = new PolynomialFunction("x+-.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+3.999966");
            }

            {
                var pf = new PolynomialFunction("x+-.34e4+4");
                Assert.AreEqual(pf.ToString(), "x-3396");
            }

            {
                var pf = new PolynomialFunction("x+-12e+4+4");
                Assert.AreEqual(pf.ToString(), "x-119996");
            }

            {
                var pf = new PolynomialFunction("x+-12e-4+4");
                Assert.AreEqual(pf.ToString(), "x+3.9988");
            }

            {
                var pf = new PolynomialFunction("x+-12e4+4");
                Assert.AreEqual(pf.ToString(), "x-119996");
            }

            {
                var pf = new PolynomialFunction("x+12.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x+123404");
            }

            {
                var pf = new PolynomialFunction("x+12.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.001234");
            }

            {
                var pf = new PolynomialFunction("x+12.34e4+4");
                Assert.AreEqual(pf.ToString(), "x+123404");
            }

            {
                var pf = new PolynomialFunction("x+.34e+4+4");
                Assert.AreEqual(pf.ToString(), "x+3404");
            }

            {
                var pf = new PolynomialFunction("x+.34e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.000034");
            }

            {
                var pf = new PolynomialFunction("x+.34e4+4");
                Assert.AreEqual(pf.ToString(), "x+3404");
            }

            {
                var pf = new PolynomialFunction("x+12e+4+4");
                Assert.AreEqual(pf.ToString(), "x+120004");
            }

            {
                var pf = new PolynomialFunction("x+12e-4+4");
                Assert.AreEqual(pf.ToString(), "x+4.0012");
            }

            {
                var pf = new PolynomialFunction("x+12e4+4");
                Assert.AreEqual(pf.ToString(), "x+120004");
            }

            {
                var pf = new PolynomialFunction("x++12.34+4");
                Assert.AreEqual(pf.ToString(), "x+16.34");
            }


            {
                var pf = new PolynomialFunction("x++.34+4");
                Assert.AreEqual(pf.ToString(), "x+4.34");
            }

            {
                var pf = new PolynomialFunction("x++12+4");
                Assert.AreEqual(pf.ToString(), "x+16");
            }

            {
                var pf = new PolynomialFunction("x+-12.34+4");
                Assert.AreEqual(pf.ToString(), "x-8.34");
            }

            {
                var pf = new PolynomialFunction("x+-.34+4");
                Assert.AreEqual(pf.ToString(), "x+3.66");
            }

            {
                var pf = new PolynomialFunction("x+-12+4");
                Assert.AreEqual(pf.ToString(), "x-8");
            }

            {
                var pf = new PolynomialFunction("x+12.34+4");
                Assert.AreEqual(pf.ToString(), "x+16.34");
            }

            {
                var pf = new PolynomialFunction("x+.34+4");
                Assert.AreEqual(pf.ToString(), "x+4.34");
            }

            {
                var pf = new PolynomialFunction("x+12+4");
                Assert.AreEqual(pf.ToString(), "x+16");
            }
        }

        private void NumberEvaluationTest()
        {
            {
                var pf = new PolynomialFunction("6*4/24");
                Assert.AreEqual(pf.ToString(), "1");
            }

            {
                var pf = new PolynomialFunction("24/4/6");
                Assert.AreEqual(pf.ToString(), "1");
            }

            {
                var pf = new PolynomialFunction("5-6+1/2*3+1*3^4/5+1");
                Assert.AreEqual(pf.ToString(), "17.7");
            }
            
            {
                var pf = new PolynomialFunction("1/2^-3+1*3^4.2/5");
                Assert.AreEqual(pf.ToString(), "28.1808412217714");
            }
            
            {
                var pf = new PolynomialFunction("1/2*3+1*3^4/5+(2+3)^4+2^2*(2+4)");
                Assert.AreEqual(pf.ToString(), "666.7");
            }

            {
                var pf = new PolynomialFunction("1-2*3");
                Assert.AreEqual(pf.ToString(), "-5");
            }

            {
                var pf = new PolynomialFunction("4/2/(2+2*3)+3-4*2*2*2^(2^2*3)");
                Assert.AreEqual(pf.ToString(), "-65532.75");
            }
        }

        private void SimpleExamplesTest()
        {
            {
                var pf = new PolynomialFunction("2*(0+0)");
                Assert.AreEqual(pf.ToString(), "0");
            }

            {
                var pf = new PolynomialFunction("2*(y*0+z*0)");
                Assert.AreEqual(pf.ToString(), "0");
            }

            {
                var pf = new PolynomialFunction("x+0+x*1+x*0");
                Assert.AreEqual(pf.ToString(), "2*x");
            }
            
            {
                var pf = new PolynomialFunction("(1-2x");
                Assert.AreEqual(pf.ToString(), "1-2*x");
            }
            
            {
                var pf = new PolynomialFunction("1-x-y");
                Assert.AreEqual(pf.ToString(), "1-x-y");
            }
            
            {
                var pf = new PolynomialFunction("x-y-2z");
                Assert.AreEqual(pf.ToString(), "x-y-2*z");
            }

            {
                var pf = new PolynomialFunction("2x2(x4+5)(5x)x^2x^2x");
                Assert.AreEqual(pf.ToString(), "20*x^7*(4*x+5)");
            }

            {
                var pf = new PolynomialFunction("x*y+x*y");
                Assert.AreEqual(pf.ToString(), "2*x*y");
            }

            {
                var pf = new PolynomialFunction("2*x*y+x*y");
                Assert.AreEqual(pf.ToString(), "3*x*y");
            }

            {
                var pf = new PolynomialFunction("x^2*x^(-2)");
                Assert.AreEqual(pf.ToString(), "1");
            }

            {
                var pf = new PolynomialFunction("2*x*y+3*x*y");
                Assert.AreEqual(pf.ToString(), "5*x*y");
            }

            {
                var pf = new PolynomialFunction("2*2*3*x*y+3*4*x*5*y");
                Assert.AreEqual(pf.ToString(), "72*x*y");
            }

            {
                var pf = new PolynomialFunction("2*x*y+3*x*y+x*y*z");
                Assert.AreEqual(pf.ToString(), "5*x*y+x*y*z");
            }

            {
                var pf = new PolynomialFunction("2*x*y+x*y*z+3*x*y");
                Assert.AreEqual(pf.ToString(), "5*x*y+x*y*z");
            }

            {
                var pf = new PolynomialFunction("2*x*y+3*x*y*z+3*x*y");
                Assert.AreEqual(pf.ToString(), "5*x*y+3*x*y*z");
            }

            {
                var pf = new PolynomialFunction("x*y+3*x*y*z+3*x*y");
                Assert.AreEqual(pf.ToString(), "3*x*y*z+4*x*y");
            }

            {
                var pf = new PolynomialFunction("2*x*y+3*x*y*z+x*y");
                Assert.AreEqual(pf.ToString(), "3*x*y+3*x*y*z");
            }

            {
                var pf = new PolynomialFunction("x*y+x*y*z+x*y");
                Assert.AreEqual(pf.ToString(), "2*x*y+x*y*z");
            }

            {
                var pf = new PolynomialFunction("x*y+x*y*z+3*x*y");
                Assert.AreEqual(pf.ToString(), "x*y*z+4*x*y");
            }

            {
                var pf = new PolynomialFunction("x*y+3*x*y*z+x*y");
                Assert.AreEqual(pf.ToString(), "2*x*y+3*x*y*z");
            }

            {
                var pf = new PolynomialFunction("2*x^3*y+x+y+3*x^3*y+x*y*z");
                Assert.AreEqual(pf.ToString(), "5*x^3*y+x+y+x*y*z");
            }

            {
                var pf = new PolynomialFunction("2*x^3*y^4+x+y+3*x^3*y^4+x*y*z");
                Assert.AreEqual(pf.ToString(), "5*x^3*y^4+x+y+x*y*z");
            }

            {
                var pf = new PolynomialFunction("2*x^3*y^5+x+y+3*x^3*y^4+x*y*z");
                Assert.AreEqual(pf.ToString(), "2*x^3*y^5+x+y+3*x^3*y^4+x*y*z");
            }

            {
                var pf = new PolynomialFunction("(x-y-z)(x+y-z)(x-y+z)").Expand();
                Assert.AreEqual(pf.ToString(), "x^3-z*x^2+-x^2*y-y^2*x+2*y*z*x+y^3-z*y^2-z^2*y+z^3-z^2*x");
            }

            {
                var pf = new PolynomialFunction("(x+y-z)(x-y+z)(x+y+z)").Expand();
                Assert.AreEqual(pf.ToString(), "x^3+y*x^2+2*z*y*x-y^2*x+x^2*z-z^2*x+z*y^2-y^3+z^2*y-z^3");
            }

            {
                var pf = new PolynomialFunction("(x-y-z)(x+y-z)(x-y+z)(x+y+z)").Expand();
                Assert.AreEqual(pf.ToString(), "x^4-2*y^2*x^2-2*z^2*x^2+y^4-2*z^2*y^2+z^4");
            }

            {
                var pf = new PolynomialFunction("(x+1)^2*(x+2)^2").Expand();
                Assert.AreEqual(pf.ToString(), "x^4+6*x^3+13*x^2+12*x+4");
            }

            {
                var pf = new PolynomialFunction("(x+1)^2+(x+2)^2").Expand();
                Assert.AreEqual(pf.ToString(), "2*x^2+6*x+5");
            }

            {
                var pf = new PolynomialFunction("x/2");
                Assert.AreEqual(pf.ToString(), "0.5*x");
                Compare(pf,
                    (v) => 0.5 * v.X,
                    (s, d, t) => 0.5 * (s.X + d.X * t),
                    (v) => new Vector3(0.5, 0, 0));
            }

            {
                var pf = new PolynomialFunction("x+3");
                Assert.AreEqual(pf.ToString(), "x+3");
                Compare(pf,
                    (v) => v.X + 3,
                    (s, d, t) => s.X + d.X * t + 3,
                    (v) => new Vector3(1, 0, 0));
            }

            {
                var pf = new PolynomialFunction("x^2");
                Assert.AreEqual(pf.ToString(), "x^2");
                Compare(pf,
                    (v) => Math.Pow(v.X, 2),
                    (s, d, t) => Math.Pow(s.X + d.X * t, 2),
                    (v) => new Vector3(2 * v.X, 0, 0));
            }
            
            {
                var pf = new PolynomialFunction("x^3");
                Assert.AreEqual(pf.ToString(), "x^3");
                Compare(pf,
                    (v) => Math.Pow(v.X, 3),
                    (s, d, t) => Math.Pow(s.X + d.X * t, 3),
                    (v) => new Vector3(3 * Math.Pow(v.X, 2), 0, 0));
            }

            {
                var pf = new PolynomialFunction("-x^2");
                Assert.AreEqual(pf.ToString(), "-x^2");
                Compare(pf, (v) => -Math.Pow(v.X, 2));
            }

            {
                var pf = new PolynomialFunction("-x+2");
                Assert.AreEqual(pf.ToString(), "2-x");
                Compare(pf, (v) => -v.X + 2);
            }

            {
                var pf = new PolynomialFunction("-x-2");
                Assert.AreEqual(pf.ToString(), "-x-2");
                Compare(pf, (v) => -v.X - 2);
            }

            {
                var pf = new PolynomialFunction("-zx-y");
                Assert.AreEqual(pf.ToString(), "-z*x-y");
                Compare(pf, (v) => -v.X * v.Z - v.Y);
            }

            {
                var pf = new PolynomialFunction("x+y+z");
                Assert.AreEqual(pf.ToString(), "x+y+z");
                Compare(pf,
                    (v) => v.X + v.Y + v.Z,
                    (s, d, t) => s.X + s.Y + s.Z + t * (d.X + d.Y + d.Z),
                    (v) => new Vector3(1, 1, 1));
            }

            {
                var pf = new PolynomialFunction("x^2+y+z");
                Assert.AreEqual(pf.ToString(), "x^2+y+z");
                Compare(pf,
                    (v) => v.X * v.X + v.Y + v.Z,
                    (s, d, t) => Math.Pow(s.X + d.X * t, 2) + s.Y + d.Y * t + s.Z + d.Z * t,
                    (v) => new Vector3(2 * v.X, 1, 1));
            }

            {
                var pf = new PolynomialFunction("(x-1)^2-1");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x-1)^2-1");
                Assert.AreEqual(pf2.ToString(), "x^2-2*x");
                Func<Vector3, double> f = (v) => Math.Pow((Math.Pow(v.X, 1) - 1), 2) - 1;
                Compare(pf, f);
                Compare(pf2, f);
            }
        }

        private void ExpandExamplesTest()
        {
            {
                var pf = new PolynomialFunction("(x^2+9/4y^2+z^2-1)^2");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x^2+2.25*y^2+z^2-1)^2");
                Func<Vector3, double> f =
                    (v) => Math.Pow((Math.Pow(v.X, 2) + 2.25 * Math.Pow(v.Y, 2) + Math.Pow(v.Z, 2) - 1), 2);

                Func<Vector3, Vector3, double, double> fi = (s, d, t) =>
                           Math.Pow((Math.Pow(s.X + d.X * t, 2) + 2.25 * Math.Pow(s.Y + d.Y * t, 2) +
                           Math.Pow(s.Z + d.Z * t, 2) - 1), 2);

                Func<Vector3, Vector3> fg = (v) => new Vector3(
                    4 * v.X * (v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1),
                    9 * v.Y * (v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1),
                    4 * v.Z * (v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1));

                Compare(pf, f, fi, fg);
                Compare(pf2, f, fi, fg);
            }
            
            {
                var pf = new PolynomialFunction("(x+1)^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x+1)^3");
                Func<Vector3, double> f = (v) => Math.Pow(v.X + 1, 3);
                Compare(pf, f);
                Compare(pf2, f);
            }
            
            {
                var pf = new PolynomialFunction("(x-1)^2");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x-1)^2");
                Func<Vector3, double> f = (v) => Math.Pow(v.X - 1, 2);
                Compare(pf, f);
                Compare(pf2, f);
            }
            
            {
                var pf = new PolynomialFunction("(x-1)^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x-1)^3");
                Func<Vector3, double> f = (v) => Math.Pow(v.X - 1, 3);
                Compare(pf, f);
                Compare(pf2, f);
            }
            
            {
                var pf = new PolynomialFunction("(x^2-1)^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x^2-1)^3");
                Func<Vector3, double> f = (v) => Math.Pow((Math.Pow(v.X, 2) - 1), 3);
                Compare(pf, f);
                Compare(pf2, f);
            }

            {
                var pf = new PolynomialFunction("(x^2+9/4y^2+z^2-1)^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x^2+2.25*y^2+z^2-1)^3");
                Func<Vector3, double> f =
                    (v) => Math.Pow((Math.Pow(v.X, 2) + 2.25 * Math.Pow(v.Y, 2) + Math.Pow(v.Z, 2) - 1), 3);
                Compare(pf, f);
                Compare(pf2, f);
            }
            
            {
                var pf = new PolynomialFunction("-x^2z^3-9/80y^2z^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "-x^2*z^3-0.1125*y^2*z^3");
                Func<Vector3, double> f =
                    (v) => -Math.Pow(v.X, 2) * Math.Pow(v.Z, 3) - 0.1125 * Math.Pow(v.Y, 2) * Math.Pow(v.Z, 3);
                Compare(pf, f);
                Compare(pf2, f);
            }

            {
                var pf = new PolynomialFunction("(x^2+9/4y^2+z^2-1)^3-x^2z^3-9/80y^2z^3");
                var pf2 = pf.Expand();
                Assert.AreEqual(pf.ToString(), "(x^2+2.25*y^2+z^2-1)^3-x^2*z^3-0.1125*y^2*z^3");

                Func<Vector3, double> f =
                    (v) => Math.Pow((Math.Pow(v.X, 2) + 2.25 * Math.Pow(v.Y, 2) + Math.Pow(v.Z, 2) - 1), 3) -
                           Math.Pow(v.X, 2) * Math.Pow(v.Z, 3) - 0.1125 * Math.Pow(v.Y, 2) * Math.Pow(v.Z, 3);

                Func<Vector3, Vector3, double, double> fi = (s, d, t) =>
                           Math.Pow((Math.Pow(s.X + d.X * t, 2) + 2.25 * Math.Pow(s.Y + d.Y * t, 2) +
                           Math.Pow(s.Z + d.Z * t, 2) - 1), 3) - Math.Pow(s.X + d.X * t, 2) *
                           Math.Pow(s.Z + d.Z * t, 3) - 0.1125 * Math.Pow(s.Y + d.Y * t, 2) *
                           Math.Pow(s.Z + d.Z * t, 3);

                Func<Vector3, Vector3> fg = (v) => new Vector3(
                    6 * v.X * Math.Pow(v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1, 2) -
                        2 * v.X * v.Z * v.Z * v.Z,
                    13.5 * v.Y * Math.Pow(v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1, 2) -
                        9.0 / 40.0 * v.Y * v.Z * v.Z * v.Z,
                    6 * v.Z * Math.Pow(v.X * v.X + 2.25 * v.Y * v.Y + v.Z * v.Z - 1, 2) -
                        3 * v.X * v.X * v.Z * v.Z - 27.0 / 80.0 * v.Y * v.Y * v.Z * v.Z);

                Compare(pf, f, fi, fg);
                Compare(pf2, f, fi, fg);
            }
        }

        private void Compare(PolynomialFunction a_pf, Func<Vector3, double> a_f, 
            Func<Vector3, Vector3, double, double> a_fi = null, Func<Vector3, Vector3> a_fg = null)
        {
            foreach (var v in m_random_vectors)
                AreAlmostRelativeEquals(a_f(v), a_pf.Evaluate(v), 1e-10);

            foreach (var d in m_random_vectors)
            {
                foreach (var s in m_random_vectors)
                {
                    foreach (var t in m_random_dists)
                    {
                        if (a_fi != null)
                        {
                            AreAlmostRelativeEquals(a_fi(s, d, t), a_pf.Intersection(s, d).Evaluate(t), 1e-4); 
                        }
                    }
                }
            }

            foreach (var p in m_random_vectors)
            {
                if (a_fg != null)
                    AreAlmostRelativeEquals(a_fg(p), a_pf.Gradient(p), 1e-10);
            }
        }
    }
}